from PySide2.QtWidgets import QTreeWidgetItem,QInputDialog,QMessageBox,QLineEdit,QMenu, QAction,QFrame,QPushButton
from PySide2.QtUiTools import QUiLoader
from PySide2.QtCore import Qt
import qtawesome as qta

from share import SI

import glob,os,traceback,sys,time,re

from functools import partial

from lib.syntaxhighlighter import PythonHighlighter

class CodeHelper:

    def __init__(self):

        self.curFilePath = None
        self.textChanged = False # 标记当前文件是否改变
        self.curTitleBarText = self.TITLEBAR_PREFIX

        self.thisFolderPath = os.path.join(SI.projectPath, self.FOLDER_NAME)
        os.makedirs(self.thisFolderPath,exist_ok=True)

        # 从文件中加载UI定义

        self.ui_loader = QUiLoader()

        self.ui = self.ui_loader.load(self.UI_FILE)

        self.nodeIcon = qta.icon(self.NODE_ICON, color='SteelBlue')

        self.ui.action_addone.triggered.connect(self.action_addone)
        self.ui.action_refresh.triggered.connect(self.listFilesOnTree)
        self.ui.action_savefile.triggered.connect(self.action_savefile)
        self.ui.action_run.triggered.connect(self.action_run)


        self.highlighter = PythonHighlighter(self.ui.te_code.document())

        self.listFilesOnTree()


        # 设置上下文菜单策略，也可以在 Qt Designer上设置
        self.ui.tree_file.setContextMenuPolicy(Qt.CustomContextMenu)
        # 定义信号处理方法
        self.ui.tree_file.customContextMenuRequested.connect(
            self.show_context_menu_onfiletree)

        # 树节点名称修改
        self.ui.tree_file.itemChanged.connect(self.treeItemNameChanged)
        # 树节点被点击
        self.ui.tree_file.itemClicked.connect(self.treeItemClicked)
        # 编辑文本框内容改动事件
        self.ui.te_code.textChanged.connect(self.handleTextChange)



        # 代码助手栏

        class QHLine(QFrame):
            def __init__(self):
                super(QHLine, self).__init__()
                self.setFrameShape(QFrame.HLine)
                self.setFrameShadow(QFrame.Sunken)

        for cs in self.codeSnippets:
            if cs == 'separator' :
                self.ui.lo_action.addWidget(QHLine())
                continue
            btn = QPushButton(cs['name'], self.ui.frame_action)
            # 定义该按钮的点击处理函数
            btn.clicked.connect(partial(self.codeHelperClicked, cs['code'] ) )
            self.ui.lo_action.addWidget(btn)
        self.ui.lo_action.addStretch() # 这样可以保证里面控件从align top 而不是 居中排列



    # 代码助手被点击
    def codeHelperClicked(self,code) :
        if not self.curFilePath:
            QMessageBox.warning(
                self.ui,
                '',
                '请先打开代码文件')
            return
        self.ui.te_code.insertPlainText(code)

    def show_context_menu_onfiletree(self,position):

        tree = self.ui.tree_file

        # 获取当前用户点选的节点
        curItem = tree.currentItem()

        # 没有当前选中节点
        if not curItem:
            print('没有选中节点，返回')
            return

        # 创建并展示 上下文菜单
        action_delnode = QAction("删除")
        action_delnode.triggered.connect(self.action_delnode)
        menu = QMenu(tree)
        menu.addAction(action_delnode)

        menu.exec_(tree.mapToGlobal(position))


    def action_delnode(self):

        tree = self.ui.tree_file

        # 获取当前用户点选的节点
        curItem = tree.currentItem()

        filepath = os.path.join(self.thisFolderPath, curItem.text(0))
        try:
            os.remove(filepath)
        except:
            pass

        # 找到改节点的父节点
        parentItem = curItem.parent()
        # 如果没有父节点，就是不可见父节点
        if not parentItem:
            parentItem = tree.invisibleRootItem()
        # 删除该节点
        parentItem.removeChild(curItem)



    def listFilesOnTree(self):
        self.ui.tree_file.clear()

        pyfiles = glob.glob(os.path.join(self.thisFolderPath, '*.py'))


        # 获取树控件的不可见根节点
        root = self.ui.tree_file.invisibleRootItem()
        for pyf in pyfiles:
            fname = os.path.basename(pyf)

            # 准备一个节点
            nodeitem = QTreeWidgetItem()
            # 设置节点图标
            nodeitem.setIcon(0, self.nodeIcon)
            # 设置该节点  第1个column 文本
            nodeitem.setText(0, fname)
            nodeitem._my_filename = fname # python 对象可以动态添加属性
            # 设置该节点在以前的flag基础上，多一个可编辑 ItemIsEditable
            nodeitem.setFlags(nodeitem.flags()  | Qt.ItemIsEditable)
            root.addChild(nodeitem)


    def action_addone(self):
        # 获得合适的初始名字
        for i in range(1,10000):
            filename = f'{self.FILE_PREFIX}_{i}.py'
            if not os.path.exists(os.path.join(self.thisFolderPath, filename)):
                break


        while True:
            filename, okPressed = QInputDialog.getText(self.ui, "请输入文件名字",
                                                    "文件名",
                                                    QLineEdit.Normal,
                                                    filename)
            filename = filename.strip()
            if not okPressed :
                return

            filepath = os.path.join(self.thisFolderPath, filename)
            if os.path.exists(filepath):
                QMessageBox.information(
                    self.ui,
                    '错误',
                    f'文件 {filepath} 已经存在，请重新输入')
            else:
                break

        open(filepath, 'w',encoding='utf8')

        parentItem = self.ui.tree_file.invisibleRootItem()

        # 准备一个节点
        nodeitem = QTreeWidgetItem()
        # 设置节点图标
        nodeitem.setIcon(0, self.nodeIcon)
        # 设置该节点  第1个column 文本
        nodeitem.setText(0, filename)
        nodeitem._my_filename = filename  # python 对象可以动态添加属性
        # 设置该节点在以前的flag基础上，多一个可编辑 ItemIsEditable
        nodeitem.setFlags(nodeitem.flags() | Qt.ItemIsEditable)

        # 添加到树的不可见根节点下，就成为第一层节点
        parentItem.addChild(nodeitem)


    # 修改节点名
    def treeItemNameChanged(self, item, column):

        newName = item.text(column)

        filepath = os.path.join(self.thisFolderPath, newName)

        # 文件名冲突，还原
        if os.path.exists(filepath):
            QMessageBox.information(
                self.ui,
                '错误',
                f'文件 {filepath} 已经存在，请重新修改')

            # 原来的名字保存在自己定义的 _my_filename 属性中
            item.setText(0, item._my_filename)
            return

        oriFilePath = os.path.join(self.thisFolderPath, item._my_filename)
        # 修改文件名
        os.rename(oriFilePath, filepath)

    # 文件节点被点击，加载新文件
    def treeItemClicked(self, item, column):
        # 获取被点击的节点文本
        fileName = item.text(column)
        filePath = os.path.join(self.thisFolderPath, fileName)

        # 如果这个要新打开的文件就是当前文件，直接返回
        if filePath == self.curFilePath:
            return

            # 如果前面的当前文件还没有保存，先保存
        if self.textChanged:
            self.action_savefile()

        try:
            with open(filePath, encoding='utf8') as f:
                self.ui.te_code.setEnabled(True)  # 解除编辑框的禁用状态
                self.ui.te_code.setPlainText(f.read())

            self.curFilePath = filePath
            self.curTitleBarText = f'{self.TITLEBAR_PREFIX} {fileName}'
            self.ui.setWindowTitle(self.curTitleBarText)
            self.textChanged = False

        except:
            SI.logInfo(traceback.format_exc())

    def handleTextChange(self):
        if self.textChanged:
            return
        self.textChanged = True
        self.ui.setWindowTitle(self.curTitleBarText+' * ')

    def action_savefile(self):
        if not self.textChanged:
            return

        if not self.curFilePath:
            return

        try:
            with open(self.curFilePath, 'w', encoding='utf8') as f:
                f.write(self.ui.te_code.toPlainText())

            self.curTitleBarText = f'{self.TITLEBAR_PREFIX} {os.path.basename(self.curFilePath)}'
            self.ui.setWindowTitle(self.curTitleBarText)
            self.textChanged = False
        except:
            SI.logInfo(traceback.format_exc())

    # 运行
    def action_run(self):
        pyFile = os.path.join(SI.projectPath, 'run.py')
        if os.path.exists(pyFile):
            os.remove(pyFile)


        # 先确保文件内容保存了
        self.action_savefile()

        code = self.ui.te_code.toPlainText()

        # 加上开头的一些固定代码行，主要是导入必须的名字
        code = self.testFileTopLines + code

        with open(pyFile, 'w', encoding='utf8') as f:
            f.write(code)

        cmd = f'cd /d {SI.projectPath} &&start call "{sys.executable}" run.py'
        SI.logInfo(f'产生代码文件 {pyFile} , 运行 ===>')
        os.system(cmd)


class Client(CodeHelper):
    FOLDER_NAME = 'client'
    FILE_PREFIX = 'client'
    TITLEBAR_PREFIX = '客户端定义'
    UI_FILE = 'ui/client.ui'
    NODE_ICON = 'fa5.user'

    codeSnippets = [

        {
            'name': '创建 HTTP 客户端',
            'code': '''# 创建客户端
client = HttpClient('127.0.0.1', # 目标地址:端口
                    timeout=10   # 超时时间，单位秒
                   ) 
'''
        },

        {
            'name': '创建 HTTPS 客户端',
            'code': '''# 创建客户端
client = HttpsClient('127.0.0.1', # 目标地址:端口
                     timeout=10   # 超时时间，单位秒
                       ) 
'''
        },

        {
            'name': '使用代理',
            'code': '''client.proxy('127.0.0.1:8888')
'''
        },


        'separator',

        {
            'name': '发送 简单请求',
            'code': '''# 请求方法对应HTTP方法，包括：get、post、put、delete 等
response = client.get(
    '/api/path1'  # 请求URL
    )
'''
        },

        {
            'name': '设置 url参数',
            'code': '''response = client.get(
    '/api/path1', 
    # 通过params传入url参数
    params={
        'param1':'value1',
        'param2':'value2'
    }
    )
'''
        },

        {
            'name': '设置 消息头',
            'code': '''response = client.get(
    '/api/path1',
    # 通过headers传入指定消息头
    headers={
        'header1':'value1',
        'header2':'value2'
    })
'''
        },

        {
            'name': '设置 消息体，urlencode 格式',
            'code': '''response = client.post(
    '/api/path1',
    # 通过data传入指定urlencode格式的消息体参数
    data={
        'param1':'value1',
        'param2':'value2'
    })
'''
        },

        {
            'name': '设置 消息体，json 格式',
            'code': '''response = client.post(
    '/api/path1',
    # 通过json传入指定json格式的消息体参数
    json={
        'param1':'value1',
        'param2':'value2'
    })
'''
        },

        {
            'name': '设置 消息体，直接写入 字节',
            'code': """response = client.post(
    '/api/path1',
    headers={
        'Content-Type':'application/xml'
    }
    # 下面填写bytes的内容，注意最后的编码格式
    data='''
    <?xml version="1.0" encoding="UTF-8"?>
    <CreateBucketConfiguration>
        <StorageClass>Standard</StorageClass>
    </CreateBucketConfiguration>
    '''.encode('utf8')
    )
"""
        },

        {
            'name': '循环发10个请求',
            'code': '''for i in range(10): 
    response = client.get('/api/path1')
    sleep(5) # 间隔5秒
'''
        },

        'separator',

        {
            'name': '查看 响应时长',
            'code': '''print(f"响应时长为 {response.responseTime} ms")
'''
        },

        {
            'name': '查看 响应状态码',
            'code': '''print(f"响应状态码为 {response.status_code} ") 
'''
        },

        {
            'name': '查看 消息体 文本内容',
            'code': '''print(f"消息体字符串为 {response.text()} ") 
'''
        },

        {
            'name': '查看 消息体 原始内容',
            'code': '''print(f"消息体字节串为 {response.raw} ") 
'''
        },

        {
            'name': '查看 消息体 json格式',
            'code': '''pprint(response.json())
'''
        },

        {
            'name': '查看 消息头',
            'code': '''# 获取消息头Content-Type值
ct = response.headers['Content-Type']    
print(f"消息头Content-Type值为 {ct} ") 
'''
        },

        {
            'name': '报告一个错误',
            'code': '''      
Stats.oneError()  # 报告一个错误，加入到统计信息中 
TestLogger.write('这里写详细信息到测试日志中，方便定位问题')        
'''
        },

        {
            'name': '测试日志添加信息',
            'code': '''TestLogger.write('这里写日志信息')
'''
        },

        'separator',

        {
            'name': '等待10秒',
            'code': '''sleep(10)
'''
        },
    ]


    testFileTopLines = '''
import time
from pprint import pprint
from time import sleep
from hyload.httpclient import HttpsClient,HttpClient
from hyload.logger import TestLogger
from hyload.stats import Stats
# 此参数只有被性能场景调用时才会传入
arg = None
'''

    # 运行
    def action_run(self):
        pyFile = os.path.join(SI.projectPath, 'run.py')
        if os.path.exists(pyFile):
            os.remove(pyFile)


        # 先确保文件内容保存了
        self.action_savefile()

        code = self.ui.te_code.toPlainText()

        # 加上开头的一些固定代码行，主要是导入必须的名字
        code = self.testFileTopLines + code

        with open(pyFile, 'w', encoding='utf8') as f:
            f.write(code)

        cmd = f'cd /d {SI.projectPath} &&start call "{sys.executable}" run.py'
        SI.logInfo(f'产生代码文件 {pyFile} , 运行 ===>')
        os.system(cmd)


class LoadTest(CodeHelper):
    FOLDER_NAME = 'loadtest'
    FILE_PREFIX = 'loadtest'
    NODE_ICON = 'fa5s.users'
    TITLEBAR_PREFIX = '压力测试场景'
    UI_FILE = 'ui/loadtest.ui'

    codeSnippets = [

        {
            'name': '启动3个客户端',
            'code': '''createClients(
    'client_1', # 客户端名称
    3,       # 客户端数量
    0.1,     # 启动间隔时间，秒
    )
'''
        },

        {
            'name': '启动3个客户端，反复执行',
            'code': '''createClientsAndKeep(
    'client_1', # 客户端名称
    3,       # 客户端数量
    0.1,     # 启动间隔时间，秒
    )
'''
        },

        {
            'name': '启动3个带参数客户端',
            'code': '''# 定义每个客户端对应的参数
args = ['user1','user2','user3']

createClients(
    'client_1', # 客户端名称
    3,       # 客户端数量    
    1,       # 启动间隔时间，秒
    args # 客户端参数
    )
'''
        },

        {
            'name': '启动3个带参数客户端，反复执行',
            'code': '''# 定义每个客户端对应的参数
args = ['user1','user2','user3']

createClientsAndKeep(
    'client_1', # 客户端名称
    3,       # 客户端数量    
    1,       # 启动间隔时间，秒
    args # 客户端参数
    )
'''
        },

        {
            'name': '等待10秒',
            'code': '''
sleep(10)
    '''
        },
    ]

    testFileTopLines = '''
from gevent import monkey
monkey.patch_all()

from gevent import spawn
import gevent

import time
from pprint import pprint
from time import sleep

from hyload.stats import Stats
from hyload.logger import TestLogger

from hyload.httpclient import HttpsClient,HttpClient

clientName2Func = {}

# 如果 args 有值，一定是列表，元素依次赋值给每次clientfunc调用
def createClients(clientName, clientNum, interval, args=None):
    clientFunc = clientName2Func[clientName]
    for i in range(clientNum):
        if args:
            spawn(clientFunc, args[i])
        else:
            spawn(clientFunc)

        if i < clientNum - 1:
            sleep(interval)

# 如果 args 有值，一定是列表，元素依次赋值给每次clientfunc调用
def createClientsAndKeep(clientName, clientNum, interval, args=None):
    clientFunc = clientName2Func[clientName]

    def realFunc(args=None):
        while True:
            try:
                clientFunc(args)
            except Exception as e:
                print(e)

    for i in range(clientNum):
        if args:
            spawn(realFunc, args[i])
        else:
            spawn(realFunc)

        if i < clientNum - 1:
            sleep(interval)

Stats.start()

################## write your code  * begin * ###################


'''

    testFileBottomLines = '''

################## write your code * end * ###################

gevent.wait()
'''

    def __init__(self):
        super().__init__()
        self.ui.action_pyfile.triggered.connect(self.createLoadScript)

    def createLoadScript(self):
        pyFile = os.path.join(SI.projectPath, 'run.py')
        if os.path.exists(pyFile):
            os.remove(pyFile)

        # 先确保文件内容保存了
        self.action_savefile()

        code = self.ui.te_code.toPlainText()

        #正则表达式搜索使用的client
        pattern = "createClients\w*\(.*?'(.*?)'"

        clientNames = re.findall(pattern,code, re.DOTALL)

        # 把client内容放到函数定义中
        preCode = '\n\n'

        for clientName in clientNames:
            funcName = clientName.replace('-','_')

            cliFullPath = os.path.join(SI.projectPath,'client',clientName+'.py')
            if not os.path.exists(cliFullPath):
                SI.logInfo(f'项目目录中找不到 {cliFullPath} ！！！')
                return False

            preCode += f'def {funcName}(arg=None):\n'


            # 读取client代码
            with open(cliFullPath,encoding='utf8') as f:
                content = f.read()
            lines = content.splitlines()
            for line in lines:
                # 每行缩进4个空格
                preCode += f'    {line}\n'


            preCode += '\n'
            preCode += f'''clientName2Func['{clientName}'] = {funcName}'''
            preCode += '\n\n'


        # 加上函数定义到代码中
        newCode = preCode + code

        # 加上开头、结尾的一些固定代码行，主要是导入必须的名字
        newCode = self.testFileTopLines + newCode + self.testFileBottomLines



        # 产生load测试代码文件
        with open(pyFile, 'w', encoding='utf8') as f:
            f.write(newCode)

        SI.logInfo(f'产生性能测试代码文件 {pyFile}')

        return True

    # 运行
    def action_run(self):

        if not self.createLoadScript():
            return

        cmd = f'cd /d {SI.projectPath} &&start call "{sys.executable}" run.py console=127.0.0.1:{SI.listenPort}'

        ts = time.strftime('%Y%m%d-%H%M%S', time.localtime())
        para_statsFile = f" statsfile={SI.projectPath}/stats_perf/{ts}.sts"
        cmd += para_statsFile

        SI.logInfo(f'运行 {cmd}')
        os.system(cmd)

